package pers.yefeng.thread;

import cn.hutool.core.io.IoUtil;
import pers.yefeng.constant.StreamConstant;
import pers.yefeng.utils.StreamUtil;
import pers.yefeng.vo.StreamParamV0;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.FIFOCache;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;

/**
 * @date 2021/05/24 14:45:38
 */
@Slf4j
public class HlsMediaThread extends MediaThread {


    public HlsMediaThread(StreamParamV0 streamParamV0) {
        super(streamParamV0);
        if (StreamConstant.VIDEO_TYPE_HLS.equals(this.videoType)) {
            this.tscCache = CacheUtil.newFIFOCache(10);
        }
    }


    protected final ConcurrentHashMap<String, Long> httHlsClientMap = new ConcurrentHashMap<>();

    /**
     * hls本地文件地址
     */
    private String thisVideoPath;


    /**
     * 缓存hls 流ts文件
     */
    private FIFOCache<Object, byte[]> tscCache;





    /**
     * hls ts文件开始下标
     */
    private int tsCont = 0;

    /**
     * http连接检查时间
     */
    private long httpCheckTime = 0;


    /**
     * 创建转码推流录制器
     *
     * @return b
     */
    protected HlsMediaThread createRecodeRecorder() {
        if (!grabberStatus) {
            return this;
        }
        createATranscoderHls();
        return this;
    }

    /**
     * hls转码
     */
    private void createATranscoderHls() {

        // 创建文件夹
        // 生成观看地址
        if (StringUtils.isBlank(playAddress)) {
            playAddress = StreamUtil.generatePlaybackAddress();
        }
        String path = StreamConstant.HLS_DIR + "/" + playAddress + "/";
        thisVideoPath = path;
        // 检查文件夹是否存在，如果不存在，则创建
        File file = new File(path);
        if (!file.exists()) {
            file.mkdir();
        }
        /*
        在使用hls切片时，发现https缓存切片时，老是要报错，解决不掉，所以仍使用http缓存切片
        java.io.IOException: Unable to unwrap data, invalid status [CLOSED]
         */
        String putUrl = "http://localhost:" + StreamConstant.HTTP_PORT + "/stream/recordHls/" + playAddress + "/" + StreamConstant.VIDEO_FILE_NAME_HLS;
        path += StreamConstant.VIDEO_FILE_NAME_HLS;
        recorder = new FFmpegFrameRecorder(putUrl, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
        recorder.setFormat("hls");

        //关于hls_wrap的说明，hls_wrap表示重复覆盖之前ts切片，这是一个过时配置，ffmpeg官方推荐使用hls_list_size 和hls_flags delete_segments代替hls_wrap
        //设置单个ts切片的时间长度（以秒为单位）。默认值为2秒
        recorder.setOption("hls_time", "2");
        //不根据gop间隔进行切片,强制使用hls_time时间进行切割ts分片
//        recorder.setOption("hls_flags", "split_by_time");

        //设置播放列表条目的最大数量。如果设置为0，则列表文件将包含所有片段，默认值为5
        // 当切片的时间不受控制时，切片数量太小，就会有卡顿的现象
        recorder.setOption("hls_list_size", "4");
        //自动删除切片，如果切片数量大于hls_list_size的数量，则会开始自动删除之前的ts切片，只保留hls_list_size个数量的切片
        recorder.setOption("hls_flags", "delete_segments");
        //ts切片自动删除阈值，默认值为1，表示早于hls_list_size+1的切片将被删除
        recorder.setOption("hls_delete_threshold", "1");
        /*hls的切片类型：
         * 'mpegts'：以MPEG-2传输流格式输出ts切片文件，可以与所有HLS版本兼容。
         * 'fmp4':以Fragmented MP4(简称：fmp4)格式输出切片文件，类似于MPEG-DASH，fmp4文件可用于HLS version 7和更高版本。
         */
        recorder.setOption("hls_segment_type", "mpegts");
        // 设置这个参数，会将文件生成到本地
        //指定ts切片生成名称规则，按数字序号生成切片,例如'file%03d.ts'，就会生成file000.ts，file001.ts，file002.ts等切片文件
//        recorder.setOption("hls_segment_filename", path + "-%5d.ts");
        // 设置第一个切片的编号
        recorder.setOption("start_number", String.valueOf(tsCont));
        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);

        // 判断是否支持转复用
        if (this.supportReuse()) {
            try {
                log.info("{} | 启动Hls转复用录制器……", sourceAddress);
                recorder.start(grabber.getFormatContext());
                recorderStatus = true;
                transferFlag = true;
            } catch (FrameRecorder.Exception e) {
                log.error("{} | 启动转复用录制器失败！", sourceAddress, e);
                setProgress(StreamConstant.SERVICE_START_PROGRESS_FAILURE, "启动转复用录制器失败！");
            }
            return;
        }


        // 转码
        log.info("{} | 启动Hls转码录制器……", sourceAddress);
        //      设置零延迟
        recorder.setVideoOption("tune", "zerolatency");
        // 快速
        recorder.setVideoOption("preset", "ultrafast");
//        recorder.setVideoOption("crf", "26");
//        recorder.setVideoOption("threads", "1");
        recorder.setFrameRate(25);// 设置帧率
        recorder.setGopSize(25);// 设置gop,与帧率相同，相当于间隔1秒chan's一个关键帧
//						recorder.setVideoBitrate(500 * 1000);// 码率500kb/s
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        try {
            recorder.start();
            recorderStatus = true;
        } catch (FrameRecorder.Exception e) {
            log.error("{} | 创建转码录制器异常！", sourceAddress, e);
            setProgress(StreamConstant.SERVICE_START_PROGRESS_FAILURE, "创建转码录制器失败！");
        }


    }


    /**
     * 开启转流服务
     *
     * @return view
     */
    public void transform() {
        if (!grabberStatus || !recorderStatus) {
            return;
        }
        log.info("{} | 开启转流操作……", sourceAddress);
        try {
            grabber.flush();
        } catch (FrameGrabber.Exception e) {
            log.error("{} | 清空拉流器缓存失败！", sourceAddress, e);
        }
        String mapKey = sourceAddress + "-" + videoType;
        setProgress(StreamConstant.SERVICE_START_PROGRESS_SUCCESS, "");
        StreamConstant.MEDIA_INFO_MAP.put(mapKey, this);
        // 服务启动完成
        starting = false;


        //时间戳计算
        int errorCount = 0;
        long currentTimeMillis;
        running = true;
        while (running) {
            currentTimeMillis = System.currentTimeMillis();

            // 出错次数过多时，重新建立连接或重启
            if (errorCount > 10 || reConnection) {
                if (transferFlag) {
                    // 重新建立连接对转复用不生效，所以进行重启服务
                    restartService = true;
                    break;
                }
                try {
                    log.info("{} | 重新建立链接……", sourceAddress);
                    grabber.restart();
                    grabber.flush();
                    // 重新建立建立成功，将错误次数清零
                    errorCount = 0;
                } catch (FrameGrabber.Exception e1) {
                    log.error("{} | 重新建立连接失败", sourceAddress, e1);
                    errorCount++;
                }
            }

            // 转流操作
            try {
                if (transferFlag) {
                    //转复用
                    AVPacket pkt = grabber.grabPacket();
                    if (null == pkt || pkt.isNull()) {
                        log.error("{} | pkt is null", sourceAddress);
                        errorCount++;
                        continue;
                    }
                    recorder.recordPacket(pkt);
                    av_packet_unref(pkt);
                } else {
                    //转码
                    Frame frame = grabber.grabFrame();
                    if (frame == null) {
                        log.error("{} | frame is null", sourceAddress);
                        errorCount++;
                        continue;
                    }
                    recorder.record(frame);
                }
            } catch (Exception e) {
                log.error("{} | 转流操作异常！", sourceAddress, e);
                errorCount++;
            }

            // 检查hls连接是否存活。每3分钟执行一次
            if (currentTimeMillis - httpCheckTime > (1000 * 60 * 3)) {
                httpCheckTime = currentTimeMillis;
                checkHlsHttp();
            }
        }

        // close包含stop和release方法。录制文件必须保证最后执行stop()方法
        try {
            recorder.close();
            grabber.close();
        } catch (IOException e) {
            log.error("{} | 转流操作结束，关闭流异常！", sourceAddress, e);
        }

        // 需要重启
        if (restartService) {
            // 1分钟之内只重启一次
            log.info("{} | 重新启动……", sourceAddress);
            restartService = false;
            this.grabberStatus = false;
            this.recorderStatus = false;
            starting = true;
            reConnection = false;
            lastRestartTime = System.currentTimeMillis();
            this.createGrabber().createRecodeRecorder().transform();
        }

        log.info("{} | 转流操作结束", sourceAddress);
        StreamConstant.MEDIA_INFO_MAP.remove(mapKey);
    }

    /**
     * 检查hls连接是否有效
     */
    private void checkHlsHttp() {
        Iterator<Map.Entry<String, Long>> iterator = httHlsClientMap.entrySet().iterator();
        long currentTimeMillis = System.currentTimeMillis();
        while (iterator.hasNext()) {
            Map.Entry<String, Long> next = iterator.next();
            if (currentTimeMillis - next.getValue() > (1000 * 60 * 3)) {
                // 超过3分钟没有请求，则移除该链接
                iterator.remove();
                httpMap.remove(next.getKey());
                log.info("{} | {} | 移除无效http-hls连接！", sourceAddress, next.getKey());
            }
        }
        hasClient();
    }


    @Override
    public void addFlvHttpClient(HttpServletRequest request, HttpServletResponse response) {

    }

    public void getHlsFile(HttpServletRequest request, HttpServletResponse response, String playName) {

        String ipAddress = StreamUtil.getIpAddress(request);
        response.addHeader("Content-Disposition", "attachment;filename=play.m3u8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        if (playName.toLowerCase().endsWith("m3u8")) {
            response.setContentType("application/x-mpegURL");
        } else {
            response.setContentType("video/MP2T");
        }
        /*
        .M3U8 application/x-mpegURL or vnd.apple.mpegURL
        .ts video/MP2T
         */
        if (null == httpMap.get(ipAddress)) {
            log.info("{} | {} 新增连接！当前连接数：{}", sourceAddress, ipAddress, httHlsClientMap.size() + 1);
        }
        httHlsClientMap.put(ipAddress, System.currentTimeMillis());
        httpMap.put(ipAddress, System.currentTimeMillis());

        byte[] fileByte = getFileByte(playName);
        if (null == fileByte) {
            return;
        }
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(fileByte);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /**
     * 获取文件数据
     *
     * @param pathFile 文件地址
     * @return
     */
//    private byte[] getFileByte(String pathFile) {
//        // 服务正在重启
//        if (starting || restartService) {
//            while (true) {
//                if (starting || restartService) {
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                } else {
//                    break;
//                }
//            }
//        }
//
//        Object bites = null;
//        if (pathFile.endsWith(".ts")) {
//            // 获取最新的ts下标
//            tsCont = Integer.parseInt(pathFile.substring(pathFile.lastIndexOf("-") + 1, pathFile.lastIndexOf(".")));
//            if (this.tscCache.containsKey(pathFile)) {
//                bites = tscCache.get(pathFile);
//            }
//        }
//        if (null != bites) {
//            return (byte[]) bites;
//        }
//        File file = new File(pathFile);
//        if (!file.exists()) {
//            return null;
//        }
//
////        try {
////            // 根据文件创建时间，判断转流服务是否停止
////            BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
////            long l = basicFileAttributes.creationTime().toMillis();
////            long currentTimeMillis = System.currentTimeMillis();
////            // 如果文件存在的时间大于10秒钟，则判断该转流服务已经停止了。进行重启
////            if ((currentTimeMillis - l) - (10 * 1000) > 0) {
////                // 1分钟之内只能重启一次
////                if (!reConnection) {
////                    log.info("{} | 有ts文件存在时间超过10秒，刷新流", sourceAddress);
////                    reConnection = true;
////                }
////            }
////
////        } catch (IOException e) {
////            log.error("{} | {} | 获取文件创建时间失败！", sourceAddress, pathFile);
////        }
//        FileReader fileReader = FileReader.create(file);
//        byte[] bytes = fileReader.readBytes();
//        // 这里可以缓存一部分数据，当多个客户端同时播放时，减少磁盘文件读取速度。当缓存满后，会清除先放进去的对象
//        tscCache.put(pathFile, bytes, (1000 * 6));
//        return bytes;
//    }

    /**
     * 获取文件数据
     *
     * @param playName 文件地址
     * @return
     */
    private byte[] getFileByte(String playName) {
        // 服务正在重启
//        while (starting || restartService) {
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
        if (playName.endsWith(".ts")) {
            // 获取最新的ts下标
            String play = playName.replace(StreamConstant.VIDEO_FILE_NAME, "");
            tsCont = Integer.parseInt(play.substring(0, play.lastIndexOf(".")));
        }
        return tscCache.get(playName);

    }


    @Override
    public void recordHls(HttpServletRequest request, String playAddress, String playName) {
        try {
            byte[] bytes = IoUtil.readBytes(request.getInputStream());
            // 缓存时间20秒
            tscCache.put(playName, bytes, (1000 * 20));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}